package mspbots.teams.client.core.data.users;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.io.ByteSource;
import io.minio.PutObjectArgs;
import io.minio.errors.MinioException;
import lombok.extern.log4j.Log4j2;
import mspbots.teams.client.core.AbstractToolsUtil;
import mspbots.teams.client.core.BaseSyncService;
import mspbots.teams.client.oauth.TokenState;
import mspbots.teams.common.*;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

/**
 * data.connector.teams.core.api.UsersService
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2020/4/14
 */
@Log4j2
@Service
public class UsersServiceImpl extends AbstractToolsUtil implements BaseSyncService {

    public Flux<JsonNode> find(Long tenantId) {
        return this.mergeUser("https://graph.microsoft.com/v1.0/users/delta")
                .contextWrite(ctx -> ctx.put("tokenState", TokenState.withDefault(tenantId)));
    }

    public Mono<JsonNode> joinedTeams(Long tenantId, String id) {
        return this.getJoinedTeams(id)
                .contextWrite(ctx -> ctx.put("tokenState", TokenState.withDefault(tenantId)));
    }

    @Override
    public Mono<Void> syncFast(TokenState tokenState) {
        ResultSync resultSync = ResultSync.withDefault("USERS-SYNC");
        return this.filterCacheGet(tokenState)
                .delayElements(Duration.ofSeconds(1))
                .doOnNext(user -> {
                    if (ObjectUtils.isEmpty(user.get("@removed"))) {
                        this.syncUserPhoto(user.get("id").asText(), tokenState);
                    }
                })
                .delayElements(Duration.ofSeconds(1))
                .doOnNext(user -> {
                    if (ObjectUtils.isEmpty(user.get("@removed"))) {
                        this.syncUserManager(user.get("id").asText(), tokenState);
                    }
                })
                .doOnNext(resultNode -> this.sendEntity(RabbitKeys.USERS_SYNC_QUEUE_KEY, ResultMsg.builder()
                        .requestId(resultSync.getRequestId()).id(resultNode.get("id").textValue())
                        .action("none").entity(resultNode).tenantId(tokenState.getTenantId())
                        .syncDate(LocalDateTime.now()).build()))
                .contextWrite(ctx -> ctx.put("resultSync", resultSync))
                .then();
    }

    public Mono<JsonNode> findManagers(Long tenantId, String userId) {
        return this.getUserManager(userId, TokenState.withDefault(tenantId));
    }

    public Flux<JsonNode> manualSyncPhoto(Long tenantId) {
        return this.dataClient.get()
                .uri(uriBuilder -> uriBuilder.path("users/{tenantId}/find").build(tenantId))
                .retrieve()
                .bodyToFlux(JsonNode.class)
                .delayElements(Duration.ofSeconds(1))
                .doOnNext(userNode -> this.syncUserPhoto(userNode.get("otherId").textValue(), TokenState.withDefault(tenantId)));
    }

    private Mono<JsonNode> getJoinedTeams(String id) {
        return this.teamsClient.get("/v1.0/users/{id}/joinedTeams", QueryParams.withDefault()
                .setSelect("id,displayName"), Map.of("id", id))
                .map(jsonNode -> jsonNode.withArray("value"));
    }

    private Mono<byte[]> getUserPhoto(String userId, TokenState tokenState) {
        Map<String, Object> params = new HashMap<>(2);
        params.put("id", userId);
        return this.teamsClient.getByte("/v1.0/users/{id}/photo/$value", params)
                .contextWrite(ctx -> ctx.put("tokenState", tokenState));
    }

    private Mono<JsonNode> getUserManager(String userId, TokenState tokenState) {
        return this.teamsClient.get("/beta/users/{id}/manager", Map.of("id", userId))
                .contextWrite(ctx -> ctx.put("tokenState", tokenState));
    }

    private void syncUserManager(String userId, TokenState tokenState) {
        ResultSync resultSync = ResultSync.withDefault("USERS-MANAGER-SYNC");
        this.getUserManager(userId, tokenState)
                .contextWrite(ctx -> ctx.put("resultSync", resultSync))
                .subscribe(resultNode -> {
                    log.debug("Sync user manager name: {}, id: {}, email: {}",
                            resultNode.get("displayName"), resultNode.get("id"), resultNode.get("userPrincipalName"));
                    ObjectNode objectNode = resultNode.deepCopy();
                    objectNode.put("subUserId", userId);
                    this.sendEntity(RabbitKeys.MANAGER_SYNC_QUEUE_KEY, ResultMsg.builder()
                            .requestId(resultSync.getRequestId()).id(objectNode.get("id").textValue())
                            .action("none").entity(objectNode).tenantId(tokenState.getTenantId())
                            .syncDate(LocalDateTime.now()).build());
                }, err -> log.error("Sync user manager err: {} ", err.getMessage()));
    }


    private Flux<JsonNode> mergeUser(String url) {
        return this.fetch(UriComponentsBuilder.fromHttpUrl(url).build().toUri());
                /*.delayElements(Duration.ofMillis(200))
                .flatMap(userNode -> this.getJoinedTeams(userNode.get("id").textValue())
                        .onErrorResume(err -> Mono.just(this.objectMapper.createArrayNode()))
                        .map(teams -> {
                            ObjectNode objectNode = userNode.deepCopy();
                            objectNode.putPOJO("joinedTeams", teams);
                            return objectNode;
                        }));*/
    }

    private Flux<JsonNode> filterCacheGet(TokenState tokenState) {
        return this.stringRedisTemplate.opsForValue()
                .get(RedisKeys.USERS_ANCHOR_REDIS_KEY_PREFIX + tokenState.getTenantId())
                .defaultIfEmpty("https://graph.microsoft.com/v1.0/users/delta")
                .flatMapMany(this::mergeUser)
                .doOnNext(resp -> this.saveCacheDeltaLink(resp, tokenState.getTenantId()))
                .contextWrite(ctx -> ctx.put("tokenState", tokenState));
    }

    private Flux<JsonNode> fetch(URI uri) {
        return super.teamsClient.fetch(uri)
                .flatMap(jsonNode -> Flux.fromIterable(jsonNode.withArray("value")));
    }

    private void saveCacheDeltaLink(JsonNode resp, Long tenantId) {
        JsonNode deltaLinkNode = resp.get("@odata.deltaLink");
        if (ObjectUtils.isEmpty(deltaLinkNode) || deltaLinkNode.isNull()) {
            return;
        }
        String deltaLinkUrl = deltaLinkNode.textValue();
        this.stringRedisTemplate.opsForValue()
                .set(RedisKeys.USERS_ANCHOR_REDIS_KEY_PREFIX + tenantId, deltaLinkUrl, Duration.ofDays(7))
                .subscribe();
    }

    private void syncUserPhoto(String userId, TokenState tokenState) {
        this.getUserPhoto(userId, tokenState)
                .subscribe(result -> {
                    try {
                        InputStream stream = ByteSource.wrap(result).openStream();
                        String objectName = "users/photo/" + userId + ".jpg";
                        this.minioClient.putObject(PutObjectArgs.builder()
                                .bucket("data-teams").object(objectName).stream(stream, result.length, -1)
                                .contentType("image/jpeg").build());
                        log.debug("Sync userId [{}] upload photo file  name [{}]", userId, objectName);
                    } catch (MinioException | NoSuchAlgorithmException | IOException | InvalidKeyException e) {
                        log.error("Sync [{}] upload photo file  err:{}", userId, e.getMessage());
                    }
                }, err -> log.error("Sync user photo err:{}", err.getMessage()));
    }


}
